利用JS做出一組爵士鼓.
HTML code如下, 類別keys內部包著九個類別為key, 但持有不同data-key屬性值的div標籤. 在外頭也有九個分別與之對應的audio標籤. 當與kbd標籤內部對應的按鍵被按下時, 會發出相對應的聲響.
<div class="keys">
<!--keys裡面包著key-->
<div data-key="65" class="key">
<kbd>A</kbd>
<span class="sound">clap</span>
</div>
</div>
<!--與key對應的audio標籤-->
<audio data-key="65" src="sounds/clap.wav"></audio>
首先在window
物件底下設置監聽keydown
事件的監聽器, window
物件為包覆整個DOM的物件, 在該處設置監聽器可以監聽DOM內觸發的所有事件. keydown
事件只要瀏覽器偵測到鍵盤被按下的瞬間就會觸發. 被觸發的瞬間, 執行自訂函式playSound
來回應.
window.addEventListener('keydown', playSound);
playSound函式就是用來播放聲音的! 要發出指定的聲音需要幾個步驟, 以按鍵'A'被按下的過程來舉例:
按照以上的邏輯, 寫出來的程式碼大致如下:
function playSound(e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
if (!audio) return;
audio.play();
}
看起來有點複雜, 其實也還行! 拆開來個別理解一下...
playSound(e)
addEventListener會把被監聽的事件物件當成值, 傳入回應的自訂函式之中. 要讓自訂函式收到該值, 可以在宣告自訂函式時提供一個代表該事件的參數, 通常會用e (或是event)命名. 在這裡playSound(e)
裡面的e代表keydown事件的物件
e.keyCode
e.keyCode屬性記錄著"按下的按鍵"的keyCode. 鍵盤上的每個按鍵都有一個相對應的keyCode, 有興趣可以在 http://keycode.info/ 看看各個按鍵的keyCode. 以'A'舉例, 就是65.
document.querySelector
document.querySelector()
是用來選取DOM的方法, 括號內部填入代表CSS選擇器的字串.
CSS Selector
CSS Selector 是一組用來選擇特定元素的CSS 符號. `audio[data-key="65"]`
就是一串CSS selector. 在CSS Selector的意思為具有屬性data-key="65"
的audio標籤.
Template Literal
Template Literial是在字串前後以` `
代替" "
, 通常內部放的東西會被當成字串, 若要插入變數只要以${變數}
隔開就好. 使用 Temlate Literial讓"在字串中插入變數"的動作更加容易.
我們希望 data-key
的值是每次按下去的按鍵的keyCode, 而不是固定的65, 因此需要在data-key的值放進代表按鍵輸入keyCode的變數. 結合CSS Selector 和 Template Literal, 可以寫成 `audio[data-key="${e.keyCode}"]`
.
data-*
屬性data-key屬性是自訂的 data-* 標頭屬性, 這種屬性通常用來儲存與該元素標籤相關的小型資料, 一個標籤可以有好幾個 data-* 屬性.
所以下面這段程式碼的意思就是, 找到data-key
屬性中存著按下按鍵keyCode的audio標籤, 並指定給audio這個變數.
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
根據原始檔提供的HTML標籤, 只有九個按鍵會有相對應的audio標籤. if(!audio) return
的意思為: 如果沒有找到對應的audio, 就返回.if()
括號內的東西會自動被轉成Boolean, 結果只有true或false, 除了falsy以外的東西, 放進去的結果都是true. , 如果按下去的按鍵沒找到相對應的HTML標籤, audio變數的值就會是 undefined
, 是falsy的一種, !audio
就是非audio.
如果沒有被返回, 表示有該標籤, 就可以在下一行用audio.play()
播放.
大致上完成了! 但有些不流暢的小地方!
實際打鼓後會發現, 有些比較長的聲音檔在播放時, 當新按鍵按下去, 新的聲音檔並不會被立即執行! 所以我們的鼓會有點lag, 沒辦法咚咚咚一直敲, 這簡直侮辱了鼓手的尊嚴!
所以我們得加點料...
function playSound(e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
if (!audio) return;
// 在 play 之前加入這行
audio.currentTime = 0;
audio.play();
}
在audio.play()上方加了一行audio.currentTime = 0
. currentTime特性代表目前播放的進度.每次播放聲音前, 將播放進度設定回原點, 然後再播放, 這麼一來就可以連續敲打了!!
但還是有個美中不足之處, 我們希望敲打時, 被敲打的按鍵會發光並放大, 讓我們知道自己正在敲打哪個樂器, 沒錯, 這就是身為鼓的使命.
所以可以...
因此得加入一些程式碼!
function playSound(e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
// 用一樣的方式找尋具有相同data-key的div元素
const key = document.querySelector(`div[data-key="${e.keyCode}"]`);
if (!audio) return;
// 在代表按鍵圖示的標籤上直接加上 playing 類別
key.classList.add('playing');
audio.currentTime = 0;
audio.play();
}
用相同的方式選取具有對應data-key的div元素, 如果具有該HTML元素, 就為它加上'playing`這個CSS類別. playing這個類別記錄了放大和邊緣發黃光的CSS! 加上的瞬間, 它就會發光!
.playing {
transform: scale(1.1);
border-color: #ffc600;
box-shadow: 0 0 1rem #ffc600;
}
但總不能一直讓它發著光, 所以必須設定發完光就光芒退散.
此時先看一下key類別的CSS.
.key {
/* 前略... */
transition: all .07s ease;
/* 後略... */
}
transition
是CSS轉場, 第一個參數代表變化時會使用到轉場的屬性, all就是全部, 表示.key內可以支援轉場效果的任何屬性, 只要發生變化, 都會以動畫的方式漸變到新的屬性去. 第二個參數代表在多少時間內要完成轉場,第三個參數代表轉場過程與時間相依的函數, 簡單來說轉場速度不會是固定的, 可以依照設定在一開始轉變得很快, 後來變很慢...之類的.
會特別提到轉場, 是因為轉場結束後會觸發一個transitionend
事件, 是我們要監聽的對象! 是的長官, 發現目標了!
在keydown監聽器上方加上這兩行!
const keys = document.querySelectorAll('.key');
keys.forEach(key => key.addEventListener('transitionend', removeTransition));
用document.querySelectorAll('.key')
把所有帶有key類別的元素都選起來, 並指定給key變數. key變數內所存的值會是一串清單(DOM List), 內容是所有帶有key類別的div元素. 注意這個清單本身並不是一個陣列(Array), 只是長得很像. 我們叫它類陣列(Array-Like). 類陣列跟陣列的差別在於, 它少了陣列本身所具有的一些原型屬性(properties) 與方法(methods), 好險Array.forEach()
是支援DOM List的!
forEach()
方法顧名思義, 能把陣列內的每個元素用自訂的函式迭代執行一次. 舉例如下:
/* 這是 forEach 的舉例 */
var exampleArray = [1, 2, 3];
exampleArray.forEach(number => number += 2); // 結果為 [3, 4, 5]
因此上述程式碼的第二行意思是: 將所有帶有key類別的div元素加上監聽transitionend
的監聽器, 只要任何key類別元素轉變完畢, 就執行removeTransition
自訂函式, 將放大和發光的效果移除! removeTransition函式的內部長這樣:
function removeTransition(e) {
if (e.propertyName !== 'transform') return;
e.target.classList.remove('playing');
}
為什麼會有 if(e.propertyName...
什麼的?
因為CSS轉場的第一個參數是all
, 當一個有對應按鈕的按鍵被按下時, 該按鍵的transform
, border-color
, box-shadow
都被新增的 playing
類別影響, 都改變了, 都有轉場, 都會有轉場結束的時候, 因此會觸發好幾個transitionend
! 可是我們只需要一個啊!
所以我們只需要留下一個屬性, 並將其它屬性觸發事件的自訂函數返回. 這裡用傳入的事件物件e
裡面的propertyName
屬性, 獨留transform
的回呼函式. 事件物件的target
屬性存有觸發事件的HTML元素本身, 在這裡即為剛轉場結束, 帶有key類別的div元素. 利用e.target.classList.remove('playing')
把放大發光效果移除.
整個串起來如下面的code, 好!
function removeTransition(e) {
if (e.propertyName !== 'transform') return;
e.target.classList.remove('playing');
}
function playSound(e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
const key = document.querySelector(`div[data-key="${e.keyCode}"]`);
if (!audio) return;
key.classList.add('playing');
audio.currentTime = 0;
audio.play();
}
const keys = document.querySelectorAll('.key');
keys.forEach(key =\> key.addEventListener('transitionend', removeTransition));
window.addEventListener('keydown', playSound);
以上就是JS 30 第一篇的心得分享! 嗯打太細了...